package com.hliushi.hotel.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hliushi.hotel.constants.HotelConstants;
import com.hliushi.hotel.entity.Hotel;
import com.hliushi.hotel.entity.HotelDoc;
import com.hliushi.hotel.mapper.HotelMapper;
import com.hliushi.hotel.service.IHotelService;
import com.hliushi.hotel.web.vo.PageResult;
import com.hliushi.hotel.web.vo.RequestVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestBuilders;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Profile({"dev", "default"})    // 指定dev 或 default 环境变量时, 表示使用 es 数据源
@Service
public class HotelServiceImpl extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {


    public static final String INDEX_NAME = "hotel";

    public static final Map<String, String> map = new LinkedHashMap<>(3);
    @Resource
    private RestHighLevelClient client;

    /**
     * 初始化map数据
     */
    @PostConstruct
    public void init() {
        // todo 一个结果排序规则问题, 我这里使用了LinkHashMap保证了
        //      map中的数据顺序为  城市 -> 星级 -> 品牌
        map.put("城市", "city");
        map.put("星级", "starName");
        map.put("品牌", "brand");
    }

    /**
     * list查询
     *
     * @param params 请求参数
     * @return PageResult
     */
    @Override
    public PageResult search(RequestVO params) {
        // 1.准备Request
        SearchRequest request = new SearchRequest(INDEX_NAME);
        // 2.准备DSL

        // 2.1.query
        String key = params.getKey();
        if (StringUtils.isBlank(key)) {
            request.source().query(QueryBuilders.matchAllQuery());
        } else {
            request.source()
                    .query(QueryBuilders.matchQuery("all", key))
                    .from((params.getPage() - 1) * params.getSize()).size(params.getSize());
        }

        // 3.发送请求, 得到响应结果
        SearchResponse response = null;
        try {
            response = client.search(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        // 4.解析结果并返回
        return this.parseSearchResponse(response);
    }

    /**
     * 有条件过滤的查询
     *
     * @param params 请求参数
     * @param filter 是否过滤
     * @return PageResult
     */
    @Override
    public PageResult search(RequestVO params, Boolean filter) {
        if (!filter) {
            return search(params);
        }
        // 1.准备Request
        SearchRequest request = new SearchRequest(INDEX_NAME);

        // 2.准备DSL
        // 2.1.query
        // 构建BoolQuery, 多个查询条件组合, 肯定是boolean查询来组合
        // 将 search 方法下面条件查询的构建 重构到 buildBasicQuery方法
        this.buildBasicQuery(request, params);

        // 距离排序 -> 构建我附近位置的酒店, 并升序排序
        if (StringUtils.isNotBlank(params.getLocation())) {
            String location = params.getLocation();
            request.source().sort(SortBuilders
                    .geoDistanceSort("location", new GeoPoint(location))
                    .order(SortOrder.ASC)
                    .unit(DistanceUnit.KILOMETERS));
        }

        // 3.发送请求, 得到响应结果
        SearchResponse response = null;
        try {
            response = client.search(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        // 4.解析结果并返回
        return this.parseSearchResponse(response);
    }

    /**
     * 查询城市, 星级, 品牌的聚合结果
     *
     * @param params params 请求条件
     * @return 聚合结果, 格式: {"城市": ["上海", "北京", ...], "星级": ["四级", xx], "品牌": ["xx", "xx"]}
     */
    @Override
    public Map<String, List<String>> filters(RequestVO params) {
        // 1.准备Request
        SearchRequest request = new SearchRequest(INDEX_NAME);

        // 2.准备DSL
        /**
         * 2.1.构建过滤条件
         * 默认情况下, Bucket聚合是对索引库的所有文档做聚合,
         * 真实场景下, 用户会输入搜索条件, 因此聚合必须是对搜索结果聚合, 那么聚合必须添加限定条件
         */
        this.buildBasicQuery(request, params);
        // 2.2.构建聚合条件
        this.buildAggQuery(request);

        // 3.发出请求, 获取响应结果
        SearchResponse response = null;
        try {
            response = client.search(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        // 4.解析响应结果
        return this.parseAggSearchResponse(response);
    }

    /**
     * 实现酒店搜索页面输入框的自动补全
     * #
     * 前端页面, 当我们在输入框关键字时, 前端会发起ajax请求, 获取自动补全的list
     *
     * @param prefix 搜索关键字
     * @return List
     */
    @Override
    public List<String> getSuggestion(String prefix) {
        // 1.准备request
        SearchRequest request = new SearchRequest(INDEX_NAME);
        // 2.编写DSL
        String suggestName = "text_suggest";
        request.source().suggest(new SuggestBuilder().addSuggestion(
                // 给自动补全的结果起一个变量名
                suggestName,
                SuggestBuilders
                        // 补全字段
                        .completionSuggestion("suggestion")
                        // 搜索关键字
                        .prefix(prefix)
                        // 跳过重复的
                        .skipDuplicates(true)
                        // 获取前10条数据
                        .size(10)
        ));

        // 3.发起请求, 获取响应结果
        SearchResponse response = null;
        try {
            response = client.search(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        // 4.解析响应结果d
        return this.parseSuggestion(response, suggestName);
    }


    /**
     * 新增
     *
     * @param id
     */
    @Override
    public void insertById(Long id) {
        // 根据id查询酒店数据
        Hotel hotel = this.getById(id);
        // 准换为文档类型
        HotelDoc hotelDoc = new HotelDoc(hotel);
        // 准备Request对象
        IndexRequest indexRequest = new IndexRequest(HotelConstants.INDEX_NAME)
                .id(hotel.getId().toString());
        // 准备Json文档
        indexRequest.source(JSONObject.toJSONString(hotelDoc), XContentType.JSON);
        try {
            client.index(indexRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    @Override
    public void deleteById(Long id) {
        if (id != null) {
            DeleteRequest request = new DeleteRequest(HotelConstants.INDEX_NAME, id.toString());
            try {
                client.delete(request, RequestOptions.DEFAULT);
            } catch (IOException e) {
                log.error("删除失败 params: {}, errMsg: {}", id, e.getMessage(), e);
                throw new RuntimeException(e);
            }
        } else {
            log.error("id 为空");
        }
    }

    /**
     * 解析自动补全的结果
     *
     * @param response    响应结果
     * @param suggestName 自定义的补全结果变量名
     * @return List
     */
    private List<String> parseSuggestion(SearchResponse response, String suggestName) {

        // todo 如何优雅的处理解析失败的异常   猜测: 方法本身 throw, 调用方 catch
        //  对于解析结果来说都要做健壮性判断的
        Suggest suggest = response.getSuggest();
        // 根据名称获取补全结果
        CompletionSuggestion suggestion = suggest.getSuggestion(suggestName);
        // 获取options集合
        List<CompletionSuggestion.Entry.Option> options = suggestion.getOptions();
        List<String> result = options.stream()
                .map(option -> option.getText().string())
                .collect(Collectors.toList());
        return result;
    }

    /**
     * 构建聚合条件
     *
     * @param request 请求条件
     */
    private void buildAggQuery(SearchRequest request) {
        for (String key : map.keySet()) {
            request.source().aggregation(AggregationBuilders
                    // 聚合名字
                    .terms(key)
                    // 聚合字段
                    .field(map.get(key))
                    .size(20));
        }
    }

    /**
     * 构建基础的basic条件查询
     *
     * @param request 请求条件
     * @param params  请求参数
     */
    private void buildBasicQuery(SearchRequest request, RequestVO params) {
        // 2.准备DSL
        // 2.1.query
        BoolQueryBuilder bool = QueryBuilders.boolQuery();
        String key = params.getKey();
        // 关键字搜索放到must中, 参与算分
        if (StringUtils.isNotBlank(key)) {
            bool.must(QueryBuilders.matchQuery("all", key));
        } else {
            bool.must(QueryBuilders.matchAllQuery());
        }

        // 其他关键字条件放到filter中, 不参与算分
        if (StringUtils.isNotBlank(params.getCity())) {
            bool.filter(QueryBuilders.termQuery("city", params.getCity()));
        }
        if (StringUtils.isNotBlank(params.getStarName())) {
            bool.filter(QueryBuilders.termQuery("starName", params.getStarName()));
        }
        if (StringUtils.isNotBlank(params.getBrand())) {
            bool.filter(QueryBuilders.termQuery("brand", params.getBrand()));
        }
        if (Objects.nonNull(params.getMaxPrice()) && Objects.nonNull(params.getMinPrice())) {
            bool.filter(QueryBuilders.rangeQuery("price")
                    .gte(params.getMinPrice())
                    .lte(params.getMaxPrice()));
        }

        // 评分和价格排序
        if ("price".equals(params.getSortBy())) {
            request.source()
                    .sort(SortBuilders.fieldSort("price")
                            .order(SortOrder.ASC));
        } else if ("score".equals(params.getSortBy())) {
            request.source()
                    .sort(SortBuilders.fieldSort("score")
                            .order(SortOrder.DESC));
        }

        // 2.广告算分控制
        FunctionScoreQueryBuilder functionScoreQuery =
                QueryBuilders.functionScoreQuery(
                        // 原始查询, 相关性算分的查询
                        bool,
                        // function score的数组
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
                                // 其中的一个function score 元素
                                new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                        // 过滤条件
                                        QueryBuilders.termQuery("isAD", true),
                                        // 算分函数
                                        ScoreFunctionBuilders.weightFactorFunction(10)
                                )
                        });

        // 设置分页和排序字段
        request.source()
                .query(functionScoreQuery)
                .from((params.getPage() - 1) * params.getSize()).size(params.getSize());
    }


    /**
     * 解析结果
     *
     * @param response 响应结果
     */
    private PageResult parseSearchResponse(SearchResponse response) {
        PageResult pageResult = new PageResult();
        // 解析结果
        SearchHits searchHits = response.getHits();
        // 查询的总条数
        long total = searchHits.getTotalHits().value;
        // System.out.println("共搜索到" + total + "条数据");
        pageResult.setTotal(total);
        // 查询的结果数组
        SearchHit[] hits = searchHits.getHits();
        List<HotelDoc> hotelDocList = Arrays.stream(hits).map(hit -> {
            // 得到source
            String sourceJson = hit.getSourceAsString();
            // 反序列化
            HotelDoc hotelDoc = JSONObject.parseObject(sourceJson, HotelDoc.class);
            // 获取排序值
            Object[] sortValues = hit.getSortValues();
            if (sortValues.length > 0) {
                Object sortValue = sortValues[0];
                hotelDoc.setDistance(sortValue);
            }
            return hotelDoc;
        }).collect(Collectors.toList());

        pageResult.setHotels(hotelDocList);
        return pageResult;
    }

    /**
     * 解析聚合响应的Response
     *
     * @param response SearchResponse
     * @return Map
     */
    public Map<String, List<String>> parseAggSearchResponse(SearchResponse response) {
        Map<String, List<String>> result = new LinkedHashMap<>(3);

        // 解析结果
        Aggregations aggregations = response.getAggregations();
        // aggName ->  城市 / 品牌 / 星级
        for (String aggName : map.keySet()) {
            log.info("================== {}", aggName);
            // 根据名称获取聚合结果
            Terms brandTerms = aggregations.get(aggName);
            // 获取桶
            List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
            // 遍历
            List<String> collect = buckets.stream()
                    .map(MultiBucketsAggregation.Bucket::getKeyAsString)
                    .collect(Collectors.toList());
            result.put(map.get(aggName), collect);
        }

        return result;
    }

}
